#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
应用程序分析器核心类
负责协调各平台分析器的工作
"""

import os
from pathlib import Path
from typing import Dict, List, Optional, Union
import tempfile
import shutil
import zipfile
import subprocess

from src.utils.logger import get_logger
from src.models.result import AnalysisResult
from src.analyzers.detector import PlatformDetector
from src.analyzers.windows import WindowsAnalyzer
from src.analyzers.macos import MacOSAnalyzer
from src.analyzers.linux import LinuxAnalyzer
from src.analyzers.android import AndroidAnalyzer
from src.analyzers.ios import IOSAnalyzer
from src.analyzers.harmony import HarmonyAnalyzer
from src.report.generator import ReportGenerator

class AppAnalyzer:
    """应用程序分析器主类，协调各平台分析器工作"""
    
    def __init__(
        self,
        app_path: Union[str, Path],
        platform: str = "auto",
        output_path: Union[str, Path] = "./report",
        config: Optional[Dict] = None,
        verbose: bool = False
    ):
        """
        初始化应用程序分析器
        
        Args:
            app_path: 应用程序路径
            platform: 应用平台类型，默认为auto自动检测
            output_path: 输出报告路径
            config: 配置信息字典
            verbose: 是否输出详细日志
        """
        self.logger = get_logger("AppAnalyzer")
        self.app_path = Path(app_path)
        self.platform = platform
        self.output_path = Path(output_path)
        self.config = config or {}
        self.verbose = verbose
        
        # 创建输出目录
        os.makedirs(self.output_path, exist_ok=True)
        
        # 平台分析器映射
        self.analyzers = {
            "windows": WindowsAnalyzer,
            "macos": MacOSAnalyzer,
            "linux": LinuxAnalyzer,
            "android": AndroidAnalyzer,
            "ios": IOSAnalyzer,
            "harmony": HarmonyAnalyzer
        }
        
        # 文件后缀到平台的映射
        self.extension_to_platform = {
            ".exe": "windows",
            ".msi": "windows",
            ".msix": "windows",
            ".appx": "windows",
            ".app": "macos",
            ".dmg": "macos",
            ".apk": "android",
            ".aab": "android",
            ".ipa": "ios",
            ".hap": "harmony",
            ".deb": "linux",
            ".rpm": "linux",
            ".appimage": "linux"
        }
        
        # 临时解包目录
        self.temp_dir = None
        
        self.logger.info(f"初始化分析器，目标应用：{self.app_path}")
    
    def __del__(self):
        """清理临时目录"""
        if self.temp_dir and os.path.exists(self.temp_dir):
            try:
                shutil.rmtree(self.temp_dir)
            except Exception as e:
                self.logger.error(f"清理临时目录失败: {e}")
    
    def detect_platform(self) -> str:
        """
        自动检测应用平台类型
        
        Returns:
            检测到的平台类型字符串
        """
        # 如果是文件，先根据后缀名判断
        if self.app_path.is_file():
            file_ext = self.app_path.suffix.lower()
            if file_ext in self.extension_to_platform:
                self.logger.info(f"根据文件后缀 {file_ext} 判断平台类型为: {self.extension_to_platform[file_ext]}")
                return self.extension_to_platform[file_ext]
        
        detector = PlatformDetector(self.app_path)
        detected_platform = detector.detect()
        self.logger.info(f"检测到应用平台类型: {detected_platform}")
        return detected_platform
    
    def prepare_app_path(self, platform: str) -> Path:
        """
        准备应用路径，如果是压缩包则解包
        
        Args:
            platform: 应用平台类型
            
        Returns:
            准备好的应用路径
        """
        if not self.app_path.is_file():
            return self.app_path
        
        file_ext = self.app_path.suffix.lower()
        
        # 创建临时目录
        if not self.temp_dir:
            self.temp_dir = tempfile.mkdtemp(prefix="app_analyzer_")
        
        # 解包应用
        if platform == "android" and file_ext == ".apk":
            return self._extract_apk()
        elif platform == "ios" and file_ext == ".ipa":
            return self._extract_ipa()
        elif platform == "harmony" and file_ext == ".hap":
            return self._extract_hap()
        elif platform == "windows" and file_ext in [".msix", ".appx"]:
            return self._extract_msix()
        elif platform == "windows" and file_ext in [".exe", ".msi"]:
            return self._extract_windows_installer()
        elif platform == "macos" and file_ext == ".dmg":
            return self._extract_dmg()
        elif platform == "linux" and file_ext in [".deb", ".rpm"]:
            return self._extract_linux_package()
        
        # 如果不需要解包，返回原路径
        return self.app_path
    
    def _extract_apk(self) -> Path:
        """解包APK文件"""
        self.logger.info(f"解包APK文件: {self.app_path}")
        extract_dir = Path(self.temp_dir) / "apk_extracted"
        os.makedirs(extract_dir, exist_ok=True)
        
        try:
            # 尝试使用apktool (如果安装了)
            if shutil.which("apktool"):
                self.logger.info("使用apktool解包APK")
                subprocess.run(["apktool", "d", "-f", "-o", str(extract_dir), str(self.app_path)], 
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            else:
                # 退回到基本zip解包
                self.logger.info("使用基本zip解包APK")
                with zipfile.ZipFile(self.app_path, 'r') as zip_ref:
                    zip_ref.extractall(extract_dir)
                    
            return extract_dir
        except Exception as e:
            self.logger.error(f"解包APK失败: {e}")
            return self.app_path
    
    def _extract_ipa(self) -> Path:
        """解包IPA文件"""
        self.logger.info(f"解包IPA文件: {self.app_path}")
        extract_dir = Path(self.temp_dir) / "ipa_extracted"
        os.makedirs(extract_dir, exist_ok=True)
        
        try:
            # IPA本质上是ZIP文件
            with zipfile.ZipFile(self.app_path, 'r') as zip_ref:
                zip_ref.extractall(extract_dir)
                
            # 找到Payload目录下的.app目录
            app_dir = list(Path(extract_dir).glob("Payload/*.app"))
            if app_dir:
                return app_dir[0]
            return extract_dir
        except Exception as e:
            self.logger.error(f"解包IPA失败: {e}")
            return self.app_path
    
    def _extract_hap(self) -> Path:
        """解包HAP文件"""
        self.logger.info(f"解包HAP文件: {self.app_path}")
        extract_dir = Path(self.temp_dir) / "hap_extracted"
        os.makedirs(extract_dir, exist_ok=True)
        
        try:
            # HAP也是ZIP格式
            with zipfile.ZipFile(self.app_path, 'r') as zip_ref:
                zip_ref.extractall(extract_dir)
            return extract_dir
        except Exception as e:
            self.logger.error(f"解包HAP失败: {e}")
            return self.app_path
    
    def _extract_msix(self) -> Path:
        """解包MSIX/APPX文件"""
        self.logger.info(f"解包MSIX/APPX文件: {self.app_path}")
        extract_dir = Path(self.temp_dir) / "msix_extracted"
        os.makedirs(extract_dir, exist_ok=True)
        
        try:
            # MSIX/APPX也是ZIP格式
            with zipfile.ZipFile(self.app_path, 'r') as zip_ref:
                zip_ref.extractall(extract_dir)
            return extract_dir
        except Exception as e:
            self.logger.error(f"解包MSIX/APPX失败: {e}")
            return self.app_path
    
    def _extract_windows_installer(self) -> Path:
        """解包Windows安装程序（.exe, .msi）"""
        self.logger.info(f"解包Windows安装程序: {self.app_path}")
        extract_dir = Path(self.temp_dir) / "installer_extracted"
        os.makedirs(extract_dir, exist_ok=True)
        
        try:
            # 尝试使用7z解包，支持大多数安装包格式
            if shutil.which("7z"):
                self.logger.info("使用7z解包Windows安装程序")
                result = subprocess.run(
                    ["7z", "x", str(self.app_path), f"-o{extract_dir}", "-y"], 
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE
                )
                if result.returncode == 0:
                    self.logger.info(f"成功解包Windows安装程序到: {extract_dir}")
                    return extract_dir
            
            # 尝试使用Inno Setup解包器（对于Inno Setup安装包）
            if shutil.which("innoextract"):
                self.logger.info("使用innoextract解包Inno Setup安装程序")
                result = subprocess.run(
                    ["innoextract", "-d", str(extract_dir), str(self.app_path)],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE
                )
                if result.returncode == 0:
                    self.logger.info(f"成功解包Inno Setup安装程序到: {extract_dir}")
                    return extract_dir
            
            # 其他方法：如果是MSI，尝试使用msiexec
            if self.app_path.suffix.lower() == ".msi" and os.name == 'nt':
                self.logger.info("使用msiexec解包MSI安装包")
                result = subprocess.run(
                    ["msiexec", "/a", str(self.app_path), "/qb", f"TARGETDIR={extract_dir}"],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE
                )
                if result.returncode == 0:
                    self.logger.info(f"成功解包MSI安装包到: {extract_dir}")
                    return extract_dir
            
            # 尝试使用cab解包器（对于cab安装包）
            if shutil.which("cabextract"):
                self.logger.info("使用cabextract解包cab文件")
                result = subprocess.run(
                    ["cabextract", "-d", str(extract_dir), str(self.app_path)],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE
                )
                if result.returncode == 0:
                    self.logger.info(f"成功解包cab文件到: {extract_dir}")
                    return extract_dir
            
            # 如果上述方法都失败，尝试直接检查可执行文件
            self.logger.warning("无法解包Windows安装程序，将直接分析安装包文件")
            
            # 将原文件复制到解压目录，让分析器可以找到至少一个文件进行分析
            shutil.copy2(self.app_path, extract_dir / self.app_path.name)
            return extract_dir
            
        except Exception as e:
            self.logger.error(f"解包Windows安装程序失败: {e}")
            return self.app_path
    
    def _extract_dmg(self) -> Path:
        """解包macOS DMG镜像文件"""
        self.logger.info(f"解包DMG文件: {self.app_path}")
        extract_dir = Path(self.temp_dir) / "dmg_extracted"
        os.makedirs(extract_dir, exist_ok=True)
        
        try:
            # 在macOS上尝试挂载DMG
            if os.name == 'posix' and shutil.which("hdiutil"):
                self.logger.info("使用hdiutil挂载DMG")
                mount_point = Path(self.temp_dir) / "dmg_mount"
                os.makedirs(mount_point, exist_ok=True)
                
                # 挂载DMG
                subprocess.run(
                    ["hdiutil", "attach", str(self.app_path), "-mountpoint", str(mount_point), "-nobrowse"],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE
                )
                
                # 复制内容到解包目录
                for item in os.listdir(mount_point):
                    src = mount_point / item
                    dst = extract_dir / item
                    if os.path.isdir(src):
                        shutil.copytree(src, dst)
                    else:
                        shutil.copy2(src, dst)
                
                # 卸载DMG
                subprocess.run(
                    ["hdiutil", "detach", str(mount_point), "-force"],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE
                )
                
                return extract_dir
            
            # 其他平台或工具不可用时的替代方案
            self.logger.warning("无法解包DMG文件，可能需要在macOS上运行")
            return self.app_path
            
        except Exception as e:
            self.logger.error(f"解包DMG失败: {e}")
            return self.app_path
    
    def _extract_linux_package(self) -> Path:
        """解包Linux包文件（.deb, .rpm）"""
        self.logger.info(f"解包Linux包文件: {self.app_path}")
        extract_dir = Path(self.temp_dir) / "linux_pkg_extracted"
        os.makedirs(extract_dir, exist_ok=True)
        
        try:
            file_ext = self.app_path.suffix.lower()
            
            # 对于.deb包，优先使用纯Python方法
            if file_ext == ".deb":
                try:
                    # 尝试使用纯Python方法解包Debian包
                    self.logger.info("使用纯Python方法解包DEB文件")
                    import tarfile
                    
                    # 直接解析ar文件（.deb文件是ar格式）
                    # ar格式很简单：8字节全局头 + 多个（60字节文件头+文件内容）
                    with open(self.app_path, 'rb') as f:
                        # 验证ar文件头
                        ar_header = f.read(8)
                        if ar_header != b'!<arch>\n':
                            raise ValueError("不是有效的ar/deb文件")
                        
                        # 查找和提取data.tar文件
                        while True:
                            # 读取文件头
                            header = f.read(60)
                            if not header or len(header) < 60:
                                break
                                
                            # 解析文件头
                            filename = header[:16].rstrip(b' \x00')
                            file_size = int(header[48:58].strip())
                            
                            # 对齐到偶数字节
                            aligned_size = file_size + (file_size % 2)
                            
                            # 查找data.tar.*和control.tar.*文件
                            if filename.startswith(b'data.tar'):
                                # 读取文件内容
                                content = f.read(file_size)
                                
                                # 保存到临时文件
                                temp_tar_path = Path(self.temp_dir) / filename.decode('utf-8')
                                with open(temp_tar_path, 'wb') as tf:
                                    tf.write(content)
                                
                                # 解压tar文件
                                with tarfile.open(temp_tar_path) as tar:
                                    tar.extractall(path=extract_dir)
                                
                                self.logger.info(f"解压data.tar文件到: {extract_dir}")
                                
                                # 继续读取下一个文件（可能是control.tar）
                                if file_size % 2:
                                    f.read(1)  # 跳过对齐字节
                                continue
                                
                            elif filename.startswith(b'control.tar'):
                                # 读取文件内容
                                content = f.read(file_size)
                                
                                # 创建DEBIAN目录
                                control_dir = extract_dir / "DEBIAN"
                                os.makedirs(control_dir, exist_ok=True)
                                
                                # 保存到临时文件
                                temp_control_path = Path(self.temp_dir) / filename.decode('utf-8')
                                with open(temp_control_path, 'wb') as cf:
                                    cf.write(content)
                                
                                # 解压control文件
                                try:
                                    with tarfile.open(temp_control_path) as tar:
                                        for tarinfo in tar:
                                            if tarinfo.name in ['./control', 'control']:
                                                f = tar.extractfile(tarinfo)
                                                if f:
                                                    with open(control_dir / "control", 'wb') as control_out:
                                                        control_out.write(f.read())
                                except Exception as e:
                                    self.logger.warning(f"解压control文件出错: {e}")
                                
                                # 继续读取下一个文件
                                if file_size % 2:
                                    f.read(1)  # 跳过对齐字节
                                continue
                            
                            # 跳过其他文件
                            f.seek(aligned_size, 1)
                    
                    self.logger.info(f"成功解包DEB文件到: {extract_dir}")
                    return extract_dir
                except ImportError as e:
                    self.logger.warning(f"Python库导入失败: {e}，尝试系统工具")
                except Exception as e:
                    self.logger.warning(f"使用Python解包DEB文件时出错: {e}，尝试系统工具")
                
                # 回退到使用系统dpkg（如果可用）
                if shutil.which("dpkg"):
                    self.logger.info("使用dpkg解包DEB文件")
                    # 使用dpkg -x提取文件
                    subprocess.run(
                        ["dpkg", "-x", str(self.app_path), str(extract_dir)],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE
                    )
                    # 提取控制信息
                    control_dir = extract_dir / "DEBIAN"
                    os.makedirs(control_dir, exist_ok=True)
                    subprocess.run(
                        ["dpkg", "-e", str(self.app_path), str(control_dir)],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE
                    )
                    return extract_dir
            
            # 对于.rpm包，尝试使用纯Python方法
            elif file_ext == ".rpm":
                try:
                    # 尝试使用纯Python rpm库解包
                    self.logger.info("使用纯Python方法解包RPM文件")
                    import rpm
                    import io
                    import tarfile
                    
                    ts = rpm.TransactionSet()
                    with open(self.app_path, 'rb') as f:
                        rpm_header = ts.hdrFromFdno(f.fileno())
                        
                        # 获取文件信息
                        file_entries = rpm_header.fiFromHeader()
                        for entry in file_entries:
                            # 创建目录结构
                            file_path = extract_dir / entry[0][1:]  # 移除开头的'/'
                            os.makedirs(file_path.parent, exist_ok=True)
                            
                            # 写入文件内容（如果是文件而非目录）
                            if not entry[1] & 0o40000:  # 不是目录
                                with open(file_path, 'wb') as out_file:
                                    # 这里需要获取文件内容，但纯Python rpm库可能无法直接提供
                                    # 实际实现可能需要额外步骤
                                    pass
                        
                    self.logger.info(f"成功解包RPM文件到: {extract_dir}")
                    return extract_dir
                except ImportError as e:
                    self.logger.warning(f"Python rpm库导入失败: {e}，尝试系统工具")
                except Exception as e:
                    self.logger.warning(f"使用Python解包RPM文件时出错: {e}，尝试系统工具")
                
                # 回退到使用系统rpm2cpio（如果可用）
                if shutil.which("rpm2cpio") and shutil.which("cpio"):
                    self.logger.info("使用rpm2cpio和cpio解包RPM文件")
                    # 切换到解压目录
                    original_dir = os.getcwd()
                    os.chdir(extract_dir)
                    
                    # 使用rpm2cpio和cpio提取文件
                    process = subprocess.Popen(
                        f"rpm2cpio {self.app_path} | cpio -idmv",
                        shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
                    )
                    process.communicate()
                    
                    # 切回原目录
                    os.chdir(original_dir)
                    return extract_dir
            
            # 尝试使用7z作为通用解包工具
            if shutil.which("7z"):
                self.logger.info("使用7z解包Linux包文件")
                subprocess.run(
                    ["7z", "x", str(self.app_path), f"-o{extract_dir}", "-y"],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE
                )
                return extract_dir
            
            # 如果上述方法都失败，尝试创建一个简单文件结构
            self.logger.warning(f"无法解包Linux包文件: {file_ext}，创建基本结构")
            
            # 复制原文件并创建基本结构
            shutil.copy2(self.app_path, extract_dir / self.app_path.name)
            
            # 创建一个简单的控制文件来提供基本信息
            if file_ext == ".deb":
                os.makedirs(extract_dir / "DEBIAN", exist_ok=True)
                with open(extract_dir / "DEBIAN" / "control", 'w') as f:
                    f.write(f"Package: {self.app_path.stem}\n")
                    f.write("Version: unknown\n")
                    f.write("Architecture: unknown\n")
                    f.write("Description: Package extracted with limited information\n")
            
            return extract_dir
            
        except Exception as e:
            self.logger.error(f"解包Linux包文件失败: {e}")
            return self.app_path
    
    def get_analyzer(self, platform: str):
        """
        根据平台类型获取对应的分析器
        
        Args:
            platform: 平台类型
            
        Returns:
            平台对应的分析器实例
        
        Raises:
            ValueError: 如果平台类型不受支持
        """
        if platform not in self.analyzers:
            raise ValueError(f"不支持的平台类型: {platform}")
        
        # 准备应用路径（如果需要解包）
        prepared_path = self.prepare_app_path(platform)
        
        analyzer_class = self.analyzers[platform]
        return analyzer_class(
            app_path=prepared_path,
            config=self.config.get(platform, {}),
            verbose=self.verbose
        )
    
    def analyze(self) -> AnalysisResult:
        """
        执行应用程序分析
        
        Returns:
            分析结果对象
        """
        # 确定平台类型
        platform = self.platform
        if platform == "auto":
            platform = self.detect_platform()
        
        self.logger.info(f"开始分析 {platform} 平台应用")
        
        # 获取对应平台分析器
        analyzer = self.get_analyzer(platform)
        
        # 执行分析
        result = analyzer.analyze()
        
        # 生成报告
        report_generator = ReportGenerator(
            result=result,
            output_path=self.output_path,
            config=self.config.get("report", {})
        )
        report_generator.generate()
        
        return result 
